--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Commit f8f7133702f8ceef3dcca97f419116180d79df33
Parents : 4c8d7d1
Author : Ivan <ivan@quad4.io>
Signature : Invalid signer <e46112d44649266d71fe2193e00a4710>, author is <ivan@quad4.io>
Date : 2026-07-08T17:21:51-05:00
feat(Self-Test): improve self-check diagnostics with new checks for SQLite roundtrip, identity file roundtrip, loopback TCP, unicode path handling, RNode support, and bot launcher arguments. Update UI components and localization files to reflect these additions, ensuring comprehensive coverage across multiple languages.
Changes
18 files changed, 890 insertions(+), 26 deletions(-)
Diff
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index f49c32d5..1579c5c0 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -99,15 +99,11 @@ jobs:
esac
backend-tests:
- name: Backend tests (Python ${{ matrix.python-version }})
+ name: Backend tests (Python ${{ env.PYTHON_VERSION }})
runs-on: ubuntu-latest
timeout-minutes: 60
permissions:
contents: read
- strategy:
- fail-fast: false
- matrix:
- python-version: ["3.11", "3.14"]
steps:
- name: Checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
@@ -115,7 +111,7 @@ jobs:
- name: Set up development environment
uses: ./.github/actions/setup-dev-environment
with:
- python-version: ${{ matrix.python-version }}
+ python-version: ${{ env.PYTHON_VERSION }}
uv-version: ${{ env.UV_VERSION }}
node-version: ${{ env.NODE_VERSION }}
pnpm-version: ${{ env.PNPM_VERSION }}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1c27e711..a1e84b43 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -22,7 +22,8 @@ All notable changes to this project will be documented in this file.
- **Bots / macOS**: Creating or starting LXMFy bots from a frozen desktop build no longer re-launches a second MeshChatX instance (and hit the storage lock). Bot subprocesses re-enter ``bot_process`` via ``--meshchatx-run-module``.
- **Self-Test**: Diagnostics now include a live **bot create / start / stop / delete** check (also covered by ``--self-check`` CI and E2E smoke).
-- **Self-Test**: Expanded cross-platform checks for identity, critical imports, storage lock, temp filesystem, public assets, LXMF router, subprocess spawn, and ``--meshchatx-run-module`` re-entry (Windows / macOS / Linux CI).
+- **Self-Test**: Expanded cross-platform checks for identity, critical imports, storage lock, temp filesystem, public assets, LXMF router, subprocess spawn, ``--meshchatx-run-module`` re-entry, SQLite roundtrip, identity file roundtrip, loopback TCP, unicode path I/O, RNode support helpers, bot launcher argv, HTTP status/config/database-health/auth-csrf/bots-status/server-security/interfaces/identities/favourites/telephone APIs, and WebSocket ``/ws`` (Windows / macOS / Linux CI).
+- **CI**: Backend test job runs once on Python **3.14** (dropped the duplicate 3.11 matrix entry).
- **RNSh / Windows**: Frozen desktop builds no longer launch rnsh via ``python -m`` (``sys.executable`` is MeshChatX itself and rejects ``-m``). Sessions re-enter the bundled rnsh module with ``--meshchatx-run-module``.
- **CI / nightly**: Daily ``nightly-YYYY.MM.DD-<sha>`` tags from ``dev`` now explicitly ``workflow_dispatch`` ``build-release.yml`` after tagging so full release assets are produced.
- **Plugins**: Plugin worker `postRequest` Promise wrapper, plugin locale loading at boot, cached UI on page open, and slot renderer recursion for nested column/list/row children.
diff --git a/meshchatx/meshchat.py b/meshchatx/meshchat.py
index 62827953..40a40610 100644
--- a/meshchatx/meshchat.py
+++ b/meshchatx/meshchat.py
@@ -965,6 +965,16 @@ class ReticulumMeshChat:
)
subprocess_result = self_check_mod.check_subprocess_spawn()
run_module_result = self_check_mod.check_meshchatx_run_module()
+ storage_base = self.storage_path or self.storage_dir
+ sqlite_result = self_check_mod.check_sqlite_roundtrip(storage_base)
+ identity_file_result = self_check_mod.check_identity_file_roundtrip(
+ storage_base
+ )
+ loopback_result = self_check_mod.check_loopback_tcp()
+ unicode_result = self_check_mod.check_unicode_path(storage_base)
+ rnode_result = self_check_mod.check_rnode_support()
+ bot_launcher_result = self_check_mod.check_bot_launcher()
+ web_results = self_check_mod.check_web_stack(self)
return {
"stack_up": {
@@ -991,6 +1001,48 @@ class ReticulumMeshChat:
"lxmf_router_good": lxmf_result,
"subprocess_good": subprocess_result,
"run_module_good": run_module_result,
+ "sqlite_roundtrip": sqlite_result,
+ "identity_roundtrip": identity_file_result,
+ "loopback_tcp": loopback_result,
+ "unicode_path_good": unicode_result,
+ "rnode_support_good": rnode_result,
+ "bot_launcher_good": bot_launcher_result,
+ "http_status_good": web_results.get(
+ "http_status_good", {"status": "failed", "reason": "missing"}
+ ),
+ "http_app_info_good": web_results.get(
+ "http_app_info_good", {"status": "failed", "reason": "missing"}
+ ),
+ "http_config_good": web_results.get(
+ "http_config_good", {"status": "failed", "reason": "missing"}
+ ),
+ "http_db_health_good": web_results.get(
+ "http_db_health_good", {"status": "failed", "reason": "missing"}
+ ),
+ "http_auth_csrf_good": web_results.get(
+ "http_auth_csrf_good", {"status": "failed", "reason": "missing"}
+ ),
+ "http_bots_status_good": web_results.get(
+ "http_bots_status_good", {"status": "failed", "reason": "missing"}
+ ),
+ "http_security_good": web_results.get(
+ "http_security_good", {"status": "failed", "reason": "missing"}
+ ),
+ "http_interfaces_good": web_results.get(
+ "http_interfaces_good", {"status": "failed", "reason": "missing"}
+ ),
+ "http_identities_good": web_results.get(
+ "http_identities_good", {"status": "failed", "reason": "missing"}
+ ),
+ "http_favourites_good": web_results.get(
+ "http_favourites_good", {"status": "failed", "reason": "missing"}
+ ),
+ "http_telephone_good": web_results.get(
+ "http_telephone_good", {"status": "failed", "reason": "missing"}
+ ),
+ "websocket_good": web_results.get(
+ "websocket_good", {"status": "failed", "reason": "missing"}
+ ),
"bots_lifecycle": {
"status": "ok" if bots_ok else "failed",
"reason": bots_reason,
diff --git a/meshchatx/src/backend/bot_handler.py b/meshchatx/src/backend/bot_handler.py
index ff1b6519..1eb18cd8 100644
--- a/meshchatx/src/backend/bot_handler.py
+++ b/meshchatx/src/backend/bot_handler.py
@@ -636,7 +636,9 @@ class BotHandler:
kernel32 = ctypes.windll.kernel32
PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
STILL_ACTIVE = 259
- handle = kernel32.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, False, int(pid))
+ handle = kernel32.OpenProcess(
+ PROCESS_QUERY_LIMITED_INFORMATION, False, int(pid)
+ )
if not handle:
return False
try:
diff --git a/meshchatx/src/backend/self_check.py b/meshchatx/src/backend/self_check.py
index 11ada3ca..dd7c9858 100644
--- a/meshchatx/src/backend/self_check.py
+++ b/meshchatx/src/backend/self_check.py
@@ -44,6 +44,24 @@ SELF_CHECK_LABELS = {
"lxmf_router_good": "LXMF Router ",
"subprocess_good": "Subprocess Spawn ",
"run_module_good": "MeshChatX Run-Module ",
+ "sqlite_roundtrip": "SQLite Roundtrip ",
+ "identity_roundtrip": "Identity File Roundtrip",
+ "loopback_tcp": "Loopback TCP Bind ",
+ "unicode_path_good": "Unicode Path I/O ",
+ "rnode_support_good": "RNode Support Module ",
+ "bot_launcher_good": "Bot Launcher Argv ",
+ "http_status_good": "HTTP /api/v1/status ",
+ "http_app_info_good": "HTTP /api/v1/app/info ",
+ "http_config_good": "HTTP /api/v1/config ",
+ "http_db_health_good": "HTTP Database Health ",
+ "http_auth_csrf_good": "HTTP Auth CSRF ",
+ "http_bots_status_good": "HTTP Bots Status ",
+ "http_security_good": "HTTP Server Security ",
+ "http_interfaces_good": "HTTP RNS Interfaces ",
+ "http_identities_good": "HTTP Identities ",
+ "http_favourites_good": "HTTP Favourites ",
+ "http_telephone_good": "HTTP Telephone Status ",
+ "websocket_good": "WebSocket /ws ",
"bots_lifecycle": "Bot Create/Start/Stop ",
}
@@ -306,3 +324,498 @@ def check_subprocess_spawn() -> dict[str, str]:
return _status(True)
except Exception as exc:
return _status(False, f"Subprocess spawn check failed: {exc}")
+
+
+def check_sqlite_roundtrip(base_dir: str | None = None) -> dict[str, str]:
+ """Create a temp SQLite DB, write a row, read it back, then delete the file."""
+ import sqlite3
+
+ root = base_dir if base_dir and os.path.isdir(base_dir) else tempfile.gettempdir()
+ path = None
+ try:
+ fd, path = tempfile.mkstemp(
+ prefix="meshchatx_self_check_", suffix=".db", dir=root
+ )
+ os.close(fd)
+ conn = sqlite3.connect(path)
+ try:
+ conn.execute("CREATE TABLE probe (id INTEGER PRIMARY KEY, note TEXT)")
+ conn.execute(
+ "INSERT INTO probe (note) VALUES (?)", ("meshchatx-sqlite-ok",)
+ )
+ conn.commit()
+ row = conn.execute("SELECT note FROM probe WHERE id = 1").fetchone()
+ finally:
+ conn.close()
+ if not row or row[0] != "meshchatx-sqlite-ok":
+ return _status(False, f"Unexpected SQLite readback: {row!r}")
+ return _status(True)
+ except Exception as exc:
+ return _status(False, f"SQLite roundtrip failed: {exc}")
+ finally:
+ if path and os.path.exists(path):
+ with contextlib.suppress(Exception):
+ os.unlink(path)
+
+
+def check_identity_file_roundtrip(base_dir: str | None = None) -> dict[str, str]:
+ """Generate a Reticulum identity, save to disk, reload, and compare hashes."""
+ try:
+ import RNS
+ except Exception as exc:
+ return _status(False, f"RNS import failed: {exc}")
+
+ root = base_dir if base_dir and os.path.isdir(base_dir) else tempfile.gettempdir()
+ path = None
+ try:
+ fd, path = tempfile.mkstemp(
+ prefix="meshchatx_id_", suffix=".identity", dir=root
+ )
+ os.close(fd)
+ identity = RNS.Identity(create_keys=True)
+ original = bytes(identity.hash)
+ with open(path, "wb") as handle:
+ handle.write(identity.get_private_key())
+ loaded = RNS.Identity(create_keys=False)
+ loaded.load(path)
+ if bytes(loaded.hash) != original:
+ return _status(False, "Reloaded identity hash mismatch")
+ return _status(True)
+ except Exception as exc:
+ return _status(False, f"Identity file roundtrip failed: {exc}")
+ finally:
+ if path and os.path.exists(path):
+ with contextlib.suppress(Exception):
+ os.unlink(path)
+
+
+def check_loopback_tcp() -> dict[str, str]:
+ """Bind and accept a short-lived TCP connection on 127.0.0.1."""
+ import socket
+ import threading
+
+ try:
+ server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ server.bind(("127.0.0.1", 0))
+ server.listen(1)
+ host, port = server.getsockname()
+ received: dict[str, bytes] = {}
+
+ def _accept():
+ conn, _addr = server.accept()
+ try:
+ received["data"] = conn.recv(64)
+ conn.sendall(b"meshchatx-tcp-ok")
+ finally:
+ conn.close()
+
+ thread = threading.Thread(target=_accept, daemon=True)
+ thread.start()
+ client = socket.create_connection((host, port), timeout=5)
+ try:
+ client.sendall(b"ping")
+ reply = client.recv(64)
+ finally:
+ client.close()
+ thread.join(timeout=5)
+ server.close()
+ if received.get("data") != b"ping":
+ return _status(False, f"Server received {received.get('data')!r}")
+ if reply != b"meshchatx-tcp-ok":
+ return _status(False, f"Unexpected client reply: {reply!r}")
+ return _status(True)
+ except Exception as exc:
+ return _status(False, f"Loopback TCP check failed: {exc}")
+
+
+def check_unicode_path(base_dir: str | None = None) -> dict[str, str]:
+ """Write and read a file whose name contains non-ASCII characters."""
+ root = base_dir if base_dir and os.path.isdir(base_dir) else tempfile.gettempdir()
+ path = os.path.join(root, "meshchatx_self_check_ユニコード.txt")
+ try:
+ payload = "meshchatx-unicode-ok-αβγ"
+ with open(path, "w", encoding="utf-8") as handle:
+ handle.write(payload)
+ with open(path, encoding="utf-8") as handle:
+ if handle.read() != payload:
+ return _status(False, "Unicode path readback mismatch")
+ return _status(True)
+ except Exception as exc:
+ return _status(False, f"Unicode path check failed: {exc}")
+ finally:
+ with contextlib.suppress(Exception):
+ if os.path.exists(path):
+ os.unlink(path)
+
+
+def check_rnode_support() -> dict[str, str]:
+ """Import rnode_support and verify transport classification helpers."""
+ try:
+ from meshchatx.src.backend import rnode_support as rn
+
+ cases = (
+ ({"port": "tcp://127.0.0.1"}, "tcp"),
+ ({"port": "ble://aa:bb:cc:dd:ee:ff"}, "ble"),
+ ({"port": "/dev/ttyUSB0"}, "serial"),
+ ({"port": "", "allow_bluetooth": "true"}, "bluetooth_classic"),
+ )
+ for iface, expected in cases:
+ got = rn._rnode_iface_transport(iface)
+ if got != expected:
+ return _status(
+ False,
+ f"transport for {iface!r} was {got!r}, expected {expected!r}",
+ )
+ if not rn.rnode_port_is_tcp("tcp://127.0.0.1"):
+ return _status(False, "rnode_port_is_tcp rejected tcp://127.0.0.1")
+ if rn.rnode_port_is_tcp("/dev/ttyUSB0"):
+ return _status(False, "rnode_port_is_tcp accepted serial path")
+ return _status(True)
+ except Exception as exc:
+ return _status(False, f"RNode support check failed: {exc}")
+
+
+def check_bot_launcher() -> dict[str, str]:
+ """Verify BotHandler launcher argv for frozen and unfrozen modes."""
+ try:
+ from unittest.mock import patch
+
+ from meshchatx.src.backend.bot_handler import (
+ BotHandler,
+ _BOT_PROCESS_MODULE,
+ _MESHCHATX_RUN_MODULE_FLAG,
+ )
+
+ handler = BotHandler.__new__(BotHandler)
+ handler.runner_path = os.path.join("fake", "bot_process.py")
+
+ with patch.object(BotHandler, "_is_frozen_executable", return_value=False):
+ unfrozen = handler._resolve_bot_launcher()
+ if unfrozen != [sys.executable, handler.runner_path]:
+ return _status(False, f"Unexpected unfrozen launcher: {unfrozen!r}")
+
+ with patch.object(BotHandler, "_is_frozen_executable", return_value=True):
+ frozen = handler._resolve_bot_launcher()
+ expected_frozen = [
+ sys.executable,
+ _MESHCHATX_RUN_MODULE_FLAG,
+ _BOT_PROCESS_MODULE,
+ ]
+ if frozen != expected_frozen:
+ return _status(False, f"Unexpected frozen launcher: {frozen!r}")
+ return _status(True)
+ except Exception as exc:
+ return _status(False, f"Bot launcher check failed: {exc}")
+
+
+def _ensure_app_session_secret(app: Any) -> None:
+ if getattr(app, "session_secret_key", None):
+ return
+ secret = None
+ try:
+ if app.config is not None:
+ secret = app.config.auth_session_secret.get()
+ except Exception:
+ secret = None
+ if not secret:
+ import secrets
+
+ secret = secrets.token_urlsafe(32)
+ with contextlib.suppress(Exception):
+ if app.config is not None:
+ app.config.auth_session_secret.set(secret)
+ app.session_secret_key = secret
+
+
+def _ensure_awaitable_method(app: Any, name: str) -> None:
+ """Ensure ``app.name`` is awaitable (unit tests often patch with sync MagicMock)."""
+ import asyncio
+
+ method = getattr(app, name, None)
+ if method is None:
+ return
+ try:
+ result = method()
+ except TypeError:
+ return
+ except Exception:
+ return
+ if asyncio.iscoroutine(result):
+ result.close()
+ return
+
+ async def _noop(*_args, **_kwargs):
+ return None
+
+ try:
+ object.__setattr__(app, name, _noop)
+ except Exception:
+ setattr(app, name, _noop)
+
+
+async def _build_probe_aio_app(app: Any):
+ import asyncio
+
+ from aiohttp import web
+ from aiohttp_session import setup as setup_session
+
+ _ensure_app_session_secret(app)
+ _ensure_awaitable_method(app, "send_config_to_websocket_clients")
+ broadcast = getattr(app, "websocket_broadcast", None)
+ if callable(broadcast):
+ try:
+ maybe = broadcast("{}")
+ if asyncio.iscoroutine(maybe):
+ maybe.close()
+ else:
+
+ async def _broadcast(_data):
+ return None
+
+ try:
+ object.__setattr__(app, "websocket_broadcast", _broadcast)
+ except Exception:
+ app.websocket_broadcast = _broadcast
+ except Exception:
+ pass
+ routes = web.RouteTableDef()
+ auth_mw, mime_mw, sec_mw, csrf_mw, ip_mw = app._define_routes(routes)
+ aio_app = web.Application(
+ middlewares=[auth_mw, mime_mw, sec_mw, csrf_mw, ip_mw],
+ )
+ setup_session(aio_app, app._encrypted_cookie_storage(use_https=False))
+ aio_app.add_routes(routes)
+ return aio_app
+
+
+_WEB_PROBE_KEYS = (
+ "http_status_good",
+ "http_app_info_good",
+ "http_config_good",
+ "http_db_health_good",
+ "http_auth_csrf_good",
+ "http_bots_status_good",
+ "http_security_good",
+ "http_interfaces_good",
+ "http_identities_good",
+ "http_favourites_good",
+ "http_telephone_good",
+ "websocket_good",
+)
+
+
+async def _probe_json_get(
+ client: Any,
+ path: str,
+ *,
+ require_keys: tuple[str, ...] = (),
+ require_nested: tuple[tuple[str, type], ...] = (),
+ validate: Callable[[dict[str, Any]], str | None] | None = None,
+ timeout: float = 15.0,
+) -> dict[str, str]:
+ import asyncio
+
+ async def _once() -> dict[str, str]:
+ resp = await client.get(path)
+ body = await resp.json()
+ if resp.status != 200 or not isinstance(body, dict):
+ return _status(False, f"{path} status={resp.status}")
+ for key in require_keys:
+ if key not in body:
+ return _status(False, f"{path} missing key {key!r}")
+ for key, expected_type in require_nested:
+ value = body.get(key)
+ if not isinstance(value, expected_type):
+ return _status(
+ False,
+ f"{path} key {key!r} type={type(value).__name__}",
+ )
+ if validate is not None:
+ reason = validate(body)
+ if reason:
+ return _status(False, reason)
+ return _status(True)
+
+ try:
+ return await asyncio.wait_for(_once(), timeout=timeout)
+ except TimeoutError:
+ return _status(False, f"{path} timed out after {timeout:.0f}s")
+ except Exception as exc:
+ return _status(False, f"{path}: {exc}")
+
+
+async def _run_web_api_probes(app: Any) -> dict[str, dict[str, str]]:
+ """Hit critical HTTP + WebSocket endpoints on an ephemeral TestServer."""
+ import asyncio
+ import json
+
+ from aiohttp import WSMsgType
+ from aiohttp.test_utils import TestClient, TestServer
+
+ results: dict[str, dict[str, str]] = {
+ key: _status(False, "not run") for key in _WEB_PROBE_KEYS
+ }
+
+ try:
+ aio_app = await _build_probe_aio_app(app)
+ except Exception as exc:
+ failed = _status(False, f"Failed to build probe app: {exc}")
+ return {key: failed for key in results}
+
+ try:
+ async with TestClient(TestServer(aio_app)) as client:
+ results["http_status_good"] = await _probe_json_get(
+ client,
+ "/api/v1/status",
+ require_keys=("status",),
+ validate=lambda body: (
+ None if body.get("status") == "ok" else f"status body={body!r}"
+ ),
+ )
+ results["http_app_info_good"] = await _probe_json_get(
+ client,
+ "/api/v1/app/info",
+ require_nested=(("app_info", dict),),
+ validate=lambda body: (
+ None
+ if body.get("app_info", {}).get("version")
+ else "app_info.version missing"
+ ),
+ )
+ results["http_config_good"] = await _probe_json_get(
+ client,
+ "/api/v1/config",
+ require_nested=(("config", dict),),
+ )
+ results["http_db_health_good"] = await _probe_json_get(
+ client,
+ "/api/v1/database/health",
+ require_nested=(("database", dict),),
+ )
+
+ try:
+ resp = await asyncio.wait_for(
+ client.get("/api/v1/auth/csrf"), timeout=15
+ )
+ body = await resp.json()
+ token = body.get("csrf_token") if isinstance(body, dict) else None
+ if resp.status != 200 or not token:
+ results["http_auth_csrf_good"] = _status(
+ False,
+ f"csrf status={resp.status}",
+ )
+ else:
+ auth_resp = await asyncio.wait_for(
+ client.get("/api/v1/auth/status"),
+ timeout=15,
+ )
+ auth_body = await auth_resp.json()
+ if auth_resp.status != 200 or "auth_enabled" not in auth_body:
+ results["http_auth_csrf_good"] = _status(
+ False,
+ f"auth/status status={auth_resp.status}",
+ )
+ else:
+ results["http_auth_csrf_good"] = _status(True)
+ except TimeoutError:
+ results["http_auth_csrf_good"] = _status(False, "auth csrf timed out")
+ except Exception as exc:
+ results["http_auth_csrf_good"] = _status(False, str(exc))
+
+ results["http_bots_status_good"] = await _probe_json_get(
+ client,
+ "/api/v1/bots/status",
+ require_keys=("status", "templates"),
+ )
+ results["http_security_good"] = await _probe_json_get(
+ client,
+ "/api/v1/server/security",
+ require_keys=("listen_host", "listen_port", "auth_enabled"),
+ )
+ results["http_interfaces_good"] = await _probe_json_get(
+ client,
+ "/api/v1/reticulum/interfaces",
+ require_nested=(("interfaces", dict),),
+ )
+ results["http_identities_good"] = await _probe_json_get(
+ client,
+ "/api/v1/identities",
+ require_nested=(("identities", list),),
+ )
+ results["http_favourites_good"] = await _probe_json_get(
+ client,
+ "/api/v1/favourites",
+ require_nested=(("favourites", list),),
+ )
+ results["http_telephone_good"] = await _probe_json_get(
+ client,
+ "/api/v1/telephone/status",
+ require_keys=("enabled",),
+ )
+
+ try:
+ ws = await asyncio.wait_for(client.ws_connect("/ws"), timeout=15)
+ try:
+ try:
+ msg = await asyncio.wait_for(ws.receive(), timeout=5)
+ except TimeoutError:
+ if ws.closed:
+ results["websocket_good"] = _status(
+ False,
+ "ws closed without first message",
+ )
+ else:
+ # Connection works; first push may be absent under test doubles.
+ results["websocket_good"] = _status(True)
+ else:
+ if msg.type not in (WSMsgType.TEXT, WSMsgType.BINARY):
+ results["websocket_good"] = _status(
+ False,
+ f"unexpected ws message type={msg.type}",
+ )
+ elif msg.type == WSMsgType.TEXT:
+ try:
+ payload = json.loads(msg.data)
+ ok = isinstance(payload, dict)
+ except Exception:
+ ok = False
+ results["websocket_good"] = _status(
+ ok,
+ "" if ok else "first ws message was not JSON object",
+ )
+ else:
+ results["websocket_good"] = _status(True)
+ finally:
+ await ws.close()
+ except Exception as exc:
+ results["websocket_good"] = _status(False, str(exc))
+ except Exception as exc:
+ failed = _status(False, f"Web probe client failed: {exc}")
+ for key in results:
+ if results[key]["status"] != "ok":
+ results[key] = failed
+
+ return results
+
+
+def check_web_stack(app: Any) -> dict[str, dict[str, str]]:
+ """Run HTTP/WebSocket probes against an ephemeral aiohttp TestServer."""
+ import asyncio
+
+ try:
+ try:
+ asyncio.get_running_loop()
+ except RuntimeError:
+ return asyncio.run(_run_web_api_probes(app))
+
+ # Already inside an event loop (e.g. aiohttp request handler).
+ import concurrent.futures
+
+ with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
+ return pool.submit(lambda: asyncio.run(_run_web_api_probes(app))).result(
+ timeout=90,
+ )
+ except Exception as exc:
+ failed = _status(False, f"Web stack check failed: {exc}")
+ return {key: failed for key in _WEB_PROBE_KEYS}
diff --git a/meshchatx/src/frontend/components/settings/SettingsPage.vue b/meshchatx/src/frontend/components/settings/SettingsPage.vue
index 2ae70621..620ab1eb 100644
--- a/meshchatx/src/frontend/components/settings/SettingsPage.vue
+++ b/meshchatx/src/frontend/components/settings/SettingsPage.vue
@@ -3064,6 +3064,24 @@ export default {
item("lxmf_router_good", "selftest.lxmf_router_good"),
item("subprocess_good", "selftest.subprocess_good"),
item("run_module_good", "selftest.run_module_good"),
+ item("sqlite_roundtrip", "selftest.sqlite_roundtrip"),
+ item("identity_roundtrip", "selftest.identity_roundtrip"),
+ item("loopback_tcp", "selftest.loopback_tcp"),
+ item("unicode_path_good", "selftest.unicode_path_good"),
+ item("rnode_support_good", "selftest.rnode_support_good"),
+ item("bot_launcher_good", "selftest.bot_launcher_good"),
+ item("http_status_good", "selftest.http_status_good"),
+ item("http_app_info_good", "selftest.http_app_info_good"),
+ item("http_config_good", "selftest.http_config_good"),
+ item("http_db_health_good", "selftest.http_db_health_good"),
+ item("http_auth_csrf_good", "selftest.http_auth_csrf_good"),
+ item("http_bots_status_good", "selftest.http_bots_status_good"),
+ item("http_security_good", "selftest.http_security_good"),
+ item("http_interfaces_good", "selftest.http_interfaces_good"),
+ item("http_identities_good", "selftest.http_identities_good"),
+ item("http_favourites_good", "selftest.http_favourites_good"),
+ item("http_telephone_good", "selftest.http_telephone_good"),
+ item("websocket_good", "selftest.websocket_good"),
item("bots_lifecycle", "selftest.bots_lifecycle"),
];
},
@@ -3141,20 +3159,39 @@ export default {
this.selfTestResults = response.data;
} catch (e) {
console.error("Failed to run system self-test", e);
+ const failed = { status: "failed", reason: e.message || String(e) };
this.selfTestResults = {
- stack_up: { status: "failed", reason: e.message || String(e) },
- config_good: { status: "failed", reason: e.message || String(e) },
- db_good: { status: "failed", reason: e.message || String(e) },
- read_write_good: { status: "failed", reason: e.message || String(e) },
- identity_good: { status: "failed", reason: e.message || String(e) },
- imports_good: { status: "failed", reason: e.message || String(e) },
- storage_lock_good: { status: "failed", reason: e.message || String(e) },
- temp_fs_good: { status: "failed", reason: e.message || String(e) },
- public_assets_good: { status: "failed", reason: e.message || String(e) },
- lxmf_router_good: { status: "failed", reason: e.message || String(e) },
- subprocess_good: { status: "failed", reason: e.message || String(e) },
- run_module_good: { status: "failed", reason: e.message || String(e) },
- bots_lifecycle: { status: "failed", reason: e.message || String(e) },
+ stack_up: { ...failed },
+ config_good: { ...failed },
+ db_good: { ...failed },
+ read_write_good: { ...failed },
+ identity_good: { ...failed },
+ imports_good: { ...failed },
+ storage_lock_good: { ...failed },
+ temp_fs_good: { ...failed },
+ public_assets_good: { ...failed },
+ lxmf_router_good: { ...failed },
+ subprocess_good: { ...failed },
+ run_module_good: { ...failed },
+ sqlite_roundtrip: { ...failed },
+ identity_roundtrip: { ...failed },
+ loopback_tcp: { ...failed },
+ unicode_path_good: { ...failed },
+ rnode_support_good: { ...failed },
+ bot_launcher_good: { ...failed },
+ http_status_good: { ...failed },
+ http_app_info_good: { ...failed },
+ http_config_good: { ...failed },
+ http_db_health_good: { ...failed },
+ http_auth_csrf_good: { ...failed },
+ http_bots_status_good: { ...failed },
+ http_security_good: { ...failed },
+ http_interfaces_good: { ...failed },
+ http_identities_good: { ...failed },
+ http_favourites_good: { ...failed },
+ http_telephone_good: { ...failed },
+ websocket_good: { ...failed },
+ bots_lifecycle: { ...failed },
};
} finally {
this.selfTestRunning = false;
diff --git a/meshchatx/src/frontend/locales/de.json b/meshchatx/src/frontend/locales/de.json
index c678e6cc..e0400491 100644
--- a/meshchatx/src/frontend/locales/de.json
+++ b/meshchatx/src/frontend/locales/de.json
@@ -712,6 +712,24 @@
"lxmf_router_good": "LXMF Router",
"subprocess_good": "Subprocess Spawn",
"run_module_good": "MeshChatX Run-Module",
+ "sqlite_roundtrip": "SQLite Roundtrip",
+ "identity_roundtrip": "Identity File Roundtrip",
+ "loopback_tcp": "Loopback TCP Bind",
+ "unicode_path_good": "Unicode Path I/O",
+ "rnode_support_good": "RNode Support Module",
+ "bot_launcher_good": "Bot Launcher Argv",
+ "http_status_good": "HTTP Status",
+ "http_app_info_good": "HTTP App Info",
+ "http_config_good": "HTTP Config",
+ "http_db_health_good": "HTTP Database Health",
+ "http_auth_csrf_good": "HTTP Auth CSRF",
+ "http_bots_status_good": "HTTP Bots Status",
+ "http_security_good": "HTTP Server Security",
+ "http_interfaces_good": "HTTP RNS Interfaces",
+ "http_identities_good": "HTTP Identities",
+ "http_favourites_good": "HTTP Favourites",
+ "http_telephone_good": "HTTP Telephone Status",
+ "websocket_good": "WebSocket",
"bots_lifecycle": "Bot Erstellen / Starten / Stoppen / Löschen",
"passed": "Bestanden",
"failed": "Fehlgeschlagen",
diff --git a/meshchatx/src/frontend/locales/en.json b/meshchatx/src/frontend/locales/en.json
index f2ddd099..b1543711 100644
--- a/meshchatx/src/frontend/locales/en.json
+++ b/meshchatx/src/frontend/locales/en.json
@@ -697,7 +697,7 @@
},
"selftest": {
"title": "System Self-Test",
- "description": "Run diagnostic checks for the network stack, database, identity, critical imports, storage lock, temp filesystem, public assets, LXMF router, subprocess spawn, run-module re-entry, and bot lifecycle.",
+ "description": "Run diagnostic checks for the network stack, database, identity, imports, storage, LXMF, subprocess/run-module, SQLite, identity files, loopback TCP, unicode paths, RNode helpers, bot launcher argv, HTTP status/config/health/auth/bots/security/interfaces/identities/favourites/telephone APIs, WebSocket, and bot lifecycle.",
"run_test_btn": "Run Diagnostics",
"running": "Running Diagnostics...",
"stack_up": "Network Stack",
@@ -712,6 +712,24 @@
"lxmf_router_good": "LXMF Router",
"subprocess_good": "Subprocess Spawn",
"run_module_good": "MeshChatX Run-Module",
+ "sqlite_roundtrip": "SQLite Roundtrip",
+ "identity_roundtrip": "Identity File Roundtrip",
+ "loopback_tcp": "Loopback TCP Bind",
+ "unicode_path_good": "Unicode Path I/O",
+ "rnode_support_good": "RNode Support Module",
+ "bot_launcher_good": "Bot Launcher Argv",
+ "http_status_good": "HTTP Status",
+ "http_app_info_good": "HTTP App Info",
+ "http_config_good": "HTTP Config",
+ "http_db_health_good": "HTTP Database Health",
+ "http_auth_csrf_good": "HTTP Auth CSRF",
+ "http_bots_status_good": "HTTP Bots Status",
+ "http_security_good": "HTTP Server Security",
+ "http_interfaces_good": "HTTP RNS Interfaces",
+ "http_identities_good": "HTTP Identities",
+ "http_favourites_good": "HTTP Favourites",
+ "http_telephone_good": "HTTP Telephone Status",
+ "websocket_good": "WebSocket",
"bots_lifecycle": "Bot Create / Start / Stop / Delete",
"passed": "Passed",
"failed": "Failed",
diff --git a/meshchatx/src/frontend/locales/es.json b/meshchatx/src/frontend/locales/es.json
index 919a0b21..ab30f6e9 100644
--- a/meshchatx/src/frontend/locales/es.json
+++ b/meshchatx/src/frontend/locales/es.json
@@ -712,6 +712,24 @@
"lxmf_router_good": "LXMF Router",
"subprocess_good": "Subprocess Spawn",
"run_module_good": "MeshChatX Run-Module",
+ "sqlite_roundtrip": "SQLite Roundtrip",
+ "identity_roundtrip": "Identity File Roundtrip",
+ "loopback_tcp": "Loopback TCP Bind",
+ "unicode_path_good": "Unicode Path I/O",
+ "rnode_support_good": "RNode Support Module",
+ "bot_launcher_good": "Bot Launcher Argv",
+ "http_status_good": "HTTP Status",
+ "http_app_info_good": "HTTP App Info",
+ "http_config_good": "HTTP Config",
+ "http_db_health_good": "HTTP Database Health",
+ "http_auth_csrf_good": "HTTP Auth CSRF",
+ "http_bots_status_good": "HTTP Bots Status",
+ "http_security_good": "HTTP Server Security",
+ "http_interfaces_good": "HTTP RNS Interfaces",
+ "http_identities_good": "HTTP Identities",
+ "http_favourites_good": "HTTP Favourites",
+ "http_telephone_good": "HTTP Telephone Status",
+ "websocket_good": "WebSocket",
"bots_lifecycle": "Bot Crear / Iniciar / Detener / Eliminar",
"passed": "Aprobado",
"failed": "Fallido",
diff --git a/meshchatx/src/frontend/locales/fi.json b/meshchatx/src/frontend/locales/fi.json
index 88af7e71..27b3ce19 100644
--- a/meshchatx/src/frontend/locales/fi.json
+++ b/meshchatx/src/frontend/locales/fi.json
@@ -712,6 +712,24 @@
"lxmf_router_good": "LXMF Router",
"subprocess_good": "Subprocess Spawn",
"run_module_good": "MeshChatX Run-Module",
+ "sqlite_roundtrip": "SQLite Roundtrip",
+ "identity_roundtrip": "Identity File Roundtrip",
+ "loopback_tcp": "Loopback TCP Bind",
+ "unicode_path_good": "Unicode Path I/O",
+ "rnode_support_good": "RNode Support Module",
+ "bot_launcher_good": "Bot Launcher Argv",
+ "http_status_good": "HTTP Status",
+ "http_app_info_good": "HTTP App Info",
+ "http_config_good": "HTTP Config",
+ "http_db_health_good": "HTTP Database Health",
+ "http_auth_csrf_good": "HTTP Auth CSRF",
+ "http_bots_status_good": "HTTP Bots Status",
+ "http_security_good": "HTTP Server Security",
+ "http_interfaces_good": "HTTP RNS Interfaces",
+ "http_identities_good": "HTTP Identities",
+ "http_favourites_good": "HTTP Favourites",
+ "http_telephone_good": "HTTP Telephone Status",
+ "websocket_good": "WebSocket",
"bots_lifecycle": "Botin luonti / käynnistys / pysäytys / poisto",
"passed": "Hyväksytty",
"failed": "Epäonnistunut",
diff --git a/meshchatx/src/frontend/locales/fr.json b/meshchatx/src/frontend/locales/fr.json
index 96425b11..499b41c7 100644
--- a/meshchatx/src/frontend/locales/fr.json
+++ b/meshchatx/src/frontend/locales/fr.json
@@ -712,6 +712,24 @@
"lxmf_router_good": "LXMF Router",
"subprocess_good": "Subprocess Spawn",
"run_module_good": "MeshChatX Run-Module",
+ "sqlite_roundtrip": "SQLite Roundtrip",
+ "identity_roundtrip": "Identity File Roundtrip",
+ "loopback_tcp": "Loopback TCP Bind",
+ "unicode_path_good": "Unicode Path I/O",
+ "rnode_support_good": "RNode Support Module",
+ "bot_launcher_good": "Bot Launcher Argv",
+ "http_status_good": "HTTP Status",
+ "http_app_info_good": "HTTP App Info",
+ "http_config_good": "HTTP Config",
+ "http_db_health_good": "HTTP Database Health",
+ "http_auth_csrf_good": "HTTP Auth CSRF",
+ "http_bots_status_good": "HTTP Bots Status",
+ "http_security_good": "HTTP Server Security",
+ "http_interfaces_good": "HTTP RNS Interfaces",
+ "http_identities_good": "HTTP Identities",
+ "http_favourites_good": "HTTP Favourites",
+ "http_telephone_good": "HTTP Telephone Status",
+ "websocket_good": "WebSocket",
"bots_lifecycle": "Bot Créer / Démarrer / Arrêter / Supprimer",
"passed": "Réussi",
"failed": "Échoué",
diff --git a/meshchatx/src/frontend/locales/it.json b/meshchatx/src/frontend/locales/it.json
index 5727000f..e0e77424 100644
--- a/meshchatx/src/frontend/locales/it.json
+++ b/meshchatx/src/frontend/locales/it.json
@@ -712,6 +712,24 @@
"lxmf_router_good": "LXMF Router",
"subprocess_good": "Subprocess Spawn",
"run_module_good": "MeshChatX Run-Module",
+ "sqlite_roundtrip": "SQLite Roundtrip",
+ "identity_roundtrip": "Identity File Roundtrip",
+ "loopback_tcp": "Loopback TCP Bind",
+ "unicode_path_good": "Unicode Path I/O",
+ "rnode_support_good": "RNode Support Module",
+ "bot_launcher_good": "Bot Launcher Argv",
+ "http_status_good": "HTTP Status",
+ "http_app_info_good": "HTTP App Info",
+ "http_config_good": "HTTP Config",
+ "http_db_health_good": "HTTP Database Health",
+ "http_auth_csrf_good": "HTTP Auth CSRF",
+ "http_bots_status_good": "HTTP Bots Status",
+ "http_security_good": "HTTP Server Security",
+ "http_interfaces_good": "HTTP RNS Interfaces",
+ "http_identities_good": "HTTP Identities",
+ "http_favourites_good": "HTTP Favourites",
+ "http_telephone_good": "HTTP Telephone Status",
+ "websocket_good": "WebSocket",
"bots_lifecycle": "Bot Crea / Avvia / Ferma / Elimina",
"passed": "Superato",
"failed": "Fallito",
diff --git a/meshchatx/src/frontend/locales/nl.json b/meshchatx/src/frontend/locales/nl.json
index 2733a83b..44a69ab4 100644
--- a/meshchatx/src/frontend/locales/nl.json
+++ b/meshchatx/src/frontend/locales/nl.json
@@ -712,6 +712,24 @@
"lxmf_router_good": "LXMF Router",
"subprocess_good": "Subprocess Spawn",
"run_module_good": "MeshChatX Run-Module",
+ "sqlite_roundtrip": "SQLite Roundtrip",
+ "identity_roundtrip": "Identity File Roundtrip",
+ "loopback_tcp": "Loopback TCP Bind",
+ "unicode_path_good": "Unicode Path I/O",
+ "rnode_support_good": "RNode Support Module",
+ "bot_launcher_good": "Bot Launcher Argv",
+ "http_status_good": "HTTP Status",
+ "http_app_info_good": "HTTP App Info",
+ "http_config_good": "HTTP Config",
+ "http_db_health_good": "HTTP Database Health",
+ "http_auth_csrf_good": "HTTP Auth CSRF",
+ "http_bots_status_good": "HTTP Bots Status",
+ "http_security_good": "HTTP Server Security",
+ "http_interfaces_good": "HTTP RNS Interfaces",
+ "http_identities_good": "HTTP Identities",
+ "http_favourites_good": "HTTP Favourites",
+ "http_telephone_good": "HTTP Telephone Status",
+ "websocket_good": "WebSocket",
"bots_lifecycle": "Bot Aanmaken / Starten / Stoppen / Verwijderen",
"passed": "Geslaagd",
"failed": "Mislukt",
diff --git a/meshchatx/src/frontend/locales/ru.json b/meshchatx/src/frontend/locales/ru.json
index 5b609334..fb6e7248 100644
--- a/meshchatx/src/frontend/locales/ru.json
+++ b/meshchatx/src/frontend/locales/ru.json
@@ -712,6 +712,24 @@
"lxmf_router_good": "LXMF Router",
"subprocess_good": "Subprocess Spawn",
"run_module_good": "MeshChatX Run-Module",
+ "sqlite_roundtrip": "SQLite Roundtrip",
+ "identity_roundtrip": "Identity File Roundtrip",
+ "loopback_tcp": "Loopback TCP Bind",
+ "unicode_path_good": "Unicode Path I/O",
+ "rnode_support_good": "RNode Support Module",
+ "bot_launcher_good": "Bot Launcher Argv",
+ "http_status_good": "HTTP Status",
+ "http_app_info_good": "HTTP App Info",
+ "http_config_good": "HTTP Config",
+ "http_db_health_good": "HTTP Database Health",
+ "http_auth_csrf_good": "HTTP Auth CSRF",
+ "http_bots_status_good": "HTTP Bots Status",
+ "http_security_good": "HTTP Server Security",
+ "http_interfaces_good": "HTTP RNS Interfaces",
+ "http_identities_good": "HTTP Identities",
+ "http_favourites_good": "HTTP Favourites",
+ "http_telephone_good": "HTTP Telephone Status",
+ "websocket_good": "WebSocket",
"bots_lifecycle": "Бот: создать / запустить / остановить / удалить",
"passed": "Успешно",
"failed": "Ошибка",
diff --git a/meshchatx/src/frontend/locales/zh.json b/meshchatx/src/frontend/locales/zh.json
index ab99d086..69e330ba 100644
--- a/meshchatx/src/frontend/locales/zh.json
+++ b/meshchatx/src/frontend/locales/zh.json
@@ -712,6 +712,24 @@
"lxmf_router_good": "LXMF Router",
"subprocess_good": "Subprocess Spawn",
"run_module_good": "MeshChatX Run-Module",
+ "sqlite_roundtrip": "SQLite Roundtrip",
+ "identity_roundtrip": "Identity File Roundtrip",
+ "loopback_tcp": "Loopback TCP Bind",
+ "unicode_path_good": "Unicode Path I/O",
+ "rnode_support_good": "RNode Support Module",
+ "bot_launcher_good": "Bot Launcher Argv",
+ "http_status_good": "HTTP Status",
+ "http_app_info_good": "HTTP App Info",
+ "http_config_good": "HTTP Config",
+ "http_db_health_good": "HTTP Database Health",
+ "http_auth_csrf_good": "HTTP Auth CSRF",
+ "http_bots_status_good": "HTTP Bots Status",
+ "http_security_good": "HTTP Server Security",
+ "http_interfaces_good": "HTTP RNS Interfaces",
+ "http_identities_good": "HTTP Identities",
+ "http_favourites_good": "HTTP Favourites",
+ "http_telephone_good": "HTTP Telephone Status",
+ "websocket_good": "WebSocket",
"bots_lifecycle": "机器人 创建 / 启动 / 停止 / 删除",
"passed": "已通过",
"failed": "已失败",
diff --git a/tests/backend/api_json_contract_schemas.py b/tests/backend/api_json_contract_schemas.py
index da8f5236..2820128e 100644
--- a/tests/backend/api_json_contract_schemas.py
+++ b/tests/backend/api_json_contract_schemas.py
@@ -212,6 +212,24 @@ SELF_TEST_SCHEMA: dict = {
"lxmf_router_good",
"subprocess_good",
"run_module_good",
+ "sqlite_roundtrip",
+ "identity_roundtrip",
+ "loopback_tcp",
+ "unicode_path_good",
+ "rnode_support_good",
+ "bot_launcher_good",
+ "http_status_good",
+ "http_app_info_good",
+ "http_config_good",
+ "http_db_health_good",
+ "http_auth_csrf_good",
+ "http_bots_status_good",
+ "http_security_good",
+ "http_interfaces_good",
+ "http_identities_good",
+ "http_favourites_good",
+ "http_telephone_good",
+ "websocket_good",
"bots_lifecycle",
],
"properties": {
@@ -227,6 +245,24 @@ SELF_TEST_SCHEMA: dict = {
"lxmf_router_good": SELF_TEST_STATUS_ITEM_SCHEMA,
"subprocess_good": SELF_TEST_STATUS_ITEM_SCHEMA,
"run_module_good": SELF_TEST_STATUS_ITEM_SCHEMA,
+ "sqlite_roundtrip": SELF_TEST_STATUS_ITEM_SCHEMA,
+ "identity_roundtrip": SELF_TEST_STATUS_ITEM_SCHEMA,
+ "loopback_tcp": SELF_TEST_STATUS_ITEM_SCHEMA,
+ "unicode_path_good": SELF_TEST_STATUS_ITEM_SCHEMA,
+ "rnode_support_good": SELF_TEST_STATUS_ITEM_SCHEMA,
+ "bot_launcher_good": SELF_TEST_STATUS_ITEM_SCHEMA,
+ "http_status_good": SELF_TEST_STATUS_ITEM_SCHEMA,
+ "http_app_info_good": SELF_TEST_STATUS_ITEM_SCHEMA,
+ "http_config_good": SELF_TEST_STATUS_ITEM_SCHEMA,
+ "http_db_health_good": SELF_TEST_STATUS_ITEM_SCHEMA,
+ "http_auth_csrf_good": SELF_TEST_STATUS_ITEM_SCHEMA,
+ "http_bots_status_good": SELF_TEST_STATUS_ITEM_SCHEMA,
+ "http_security_good": SELF_TEST_STATUS_ITEM_SCHEMA,
+ "http_interfaces_good": SELF_TEST_STATUS_ITEM_SCHEMA,
+ "http_identities_good": SELF_TEST_STATUS_ITEM_SCHEMA,
+ "http_favourites_good": SELF_TEST_STATUS_ITEM_SCHEMA,
+ "http_telephone_good": SELF_TEST_STATUS_ITEM_SCHEMA,
+ "websocket_good": SELF_TEST_STATUS_ITEM_SCHEMA,
"bots_lifecycle": SELF_TEST_STATUS_ITEM_SCHEMA,
},
"additionalProperties": False,
diff --git a/tests/backend/test_self_check.py b/tests/backend/test_self_check.py
index 1c013de4..2ab2ee4f 100644
--- a/tests/backend/test_self_check.py
+++ b/tests/backend/test_self_check.py
@@ -96,6 +96,56 @@ def test_check_meshchatx_run_module_ok():
assert result["status"] == "ok", result["reason"]
+def test_check_sqlite_roundtrip_ok(tmp_path):
+ assert self_check.check_sqlite_roundtrip(str(tmp_path))["status"] == "ok"
+
+
+def test_check_identity_file_roundtrip_ok(tmp_path):
+ result = self_check.check_identity_file_roundtrip(str(tmp_path))
+ assert result["status"] == "ok", result["reason"]
+
+
+def test_check_loopback_tcp_ok():
+ assert self_check.check_loopback_tcp()["status"] == "ok"
+
+
+def test_check_unicode_path_ok(tmp_path):
+ assert self_check.check_unicode_path(str(tmp_path))["status"] == "ok"
+
+
+def test_check_rnode_support_ok():
+ assert self_check.check_rnode_support()["status"] == "ok"
+
+
+def test_check_bot_launcher_ok():
+ assert self_check.check_bot_launcher()["status"] == "ok"
+
+
+def test_check_web_stack_ok(mock_app, require_loopback_tcp):
+ from unittest.mock import AsyncMock, MagicMock
+
+ # Avoid MagicMock telephone objects taking the "enabled" code path.
+ mock_app.telephone_manager.telephone = None
+ # /api/v1/config and /api/v1/app/info must JSON-serialize reticulum/identity fields.
+ mock_app.current_context.identity.get_public_key = MagicMock(
+ return_value=bytes(32),
+ )
+ mock_app.current_context.local_lxmf_destination.hexhash = "a" * 32
+ mock_app.current_context.message_router.propagation_destination.hexhash = "b" * 32
+ if getattr(mock_app, "reticulum", None) is not None:
+ mock_app.reticulum.is_connected_to_shared_instance = False
+ mock_app.reticulum.transport_enabled = MagicMock(return_value=False)
+ mock_app.reticulum.get_path_table = MagicMock(return_value=[])
+ # WebSocket handler awaits these.
+ mock_app.send_config_to_websocket_clients = AsyncMock(return_value=None)
+ mock_app.websocket_broadcast = AsyncMock()
+ results = self_check.check_web_stack(mock_app)
+ expected = set(self_check._WEB_PROBE_KEYS)
+ assert set(results) == expected
+ for key, value in results.items():
+ assert value["status"] == "ok", f"{key}: {value.get('reason')}"
+
+
def test_self_check_labels_cover_schema_keys():
from tests.backend.api_json_contract_schemas import SELF_TEST_SCHEMA
diff --git a/tests/e2e/smoke.spec.js b/tests/e2e/smoke.spec.js
index 7b790f30..aede67bf 100644
--- a/tests/e2e/smoke.spec.js
+++ b/tests/e2e/smoke.spec.js
@@ -28,14 +28,29 @@ test.describe("MeshChatX E2E (Vite + Python backend)", () => {
"lxmf_router_good",
"subprocess_good",
"run_module_good",
+ "sqlite_roundtrip",
+ "identity_roundtrip",
+ "loopback_tcp",
+ "unicode_path_good",
+ "rnode_support_good",
+ "bot_launcher_good",
+ "http_status_good",
+ "http_app_info_good",
+ "http_config_good",
+ "http_db_health_good",
+ "http_auth_csrf_good",
+ "http_bots_status_good",
+ "http_security_good",
+ "http_interfaces_good",
+ "http_identities_good",
+ "http_favourites_good",
+ "http_telephone_good",
+ "websocket_good",
"bots_lifecycle",
];
for (const key of keys) {
expect(body[key], key).toBeDefined();
- expect(
- body[key].status,
- `${key}: ${body[key].reason || "(no reason)"}`,
- ).toBe("ok");
+ expect(body[key].status, `${key}: ${body[key].reason || "(no reason)"}`).toBe("ok");
}
});
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────